使用sklearn做自然语言处理-1
今天我们以20newsgroups数据集为例,借助sklearn库来进行话题分类(文本分类)。在本篇教程中涉及到:
读取数据
数据预处理
特征抽取
模型评估
1. 读取数据
为了便于分析,已将数据集保存到csv文件中,这样我们可以使用强大的pandas库操作数据。
import pandas as pd
dataset = pd.read_csv('20news-18828.csv', header=None, delimiter=',', names=['label', 'text'])
dataset.head()
运行结果
检查数据集的类别数和数据量(记录数)
#注意: dataset.label等同于 dataset['label']
print("数据集有20类: %s" % (len(dataset.label.unique()) == 20))
print("数据集一共有18828条记录: %s" % (len(dataset) == 18828))
运行结果
数据集有20类: True
数据集一共有18828条记录: True
做机器学习,我们一般需要将数据集分为训练集和测试集。过去和现在的数据可以作为训练集去训练模型,而未来的数据按道理是不可能出现在训练集中的,所以一般测试集我们可以理解成未知的或者未来的数据,是用来检验我们模型的数据。
这里使用sklearn.model_selection中的train_test_split函数。
train_test_split(X, y, test_size)
X: 一般为矩阵,矩阵的行数与y的长度一致
y: 一般为一维数组。
test_size: 将数据集的多少比例划分为测试集
使用train_test_split函数
from sklearn.model_selection import train_test_split
#依次导入X, y, test_size
X_train, X_test, y_train, y_test = train_test_split(dataset.text, dataset.label, test_size=0.3)
2. CountVectorizer和TfidfVectorizer
CountVectorizer和TfidfVectorizer是文本特征提取的两种方法。两者的主要区别在于,CountVectorizer仅仅通过计算词语词频,没有考虑该词语是否有代表性。而TfidfVectorizer可以更加精准的表征一个词语对某个话题的代表性。
对CountVectorizer和TfidfVectorizer还是不熟悉的童鞋,可先戳进去阅读 《如何从文本中提取特征信息?》这篇文章。
from sklearn.feature_extraction.text import CountVectorizer
cv = CountVectorizer()
#fit_transform是fit和transform两种方法的简写
#fit方法用于构建特征空间(也就是构建词典)
#transform方法使用该空间将文本数据转化为特征矩阵
X_train_counts = cv.fit_transform(X_train)
#该矩阵共有13179行,对应着13179条数据。
#175237表示特征空间共有175237个词语。
X_train_counts.shape
运行结果
(13179, 175237)
训练集其实主要是学会数据中的特征空间(构建词典),之后我们需要用学到的特征空间去处理测试集。所以我们只需要用到transform方法,从而将测试集数据从文本数据转化为特征矩阵。
X_test_counts = cv.transform(X_test)
#这里你会发现,测试集特征矩阵的列数与训练集特征矩阵的列数是一致的,都是175237
X_test_counts.shape
运行结果
(5649, 175237)
之后,我们使用朴素贝叶斯分类器去学习训练集数据,从而训练好模型。之后我们使用训练好的模型去对测试集进行预测。
from sklearn.naive_bayes import MultinomialNB
estimator = MultinomialNB()
#这里是有监督的训练模型
#所以要同时输入X_train_counts和y_train
estimator.fit(X_train_counts, y_train)
#训练好的模型,对测试集数据进行预测
predicted = estimator.predict(X_test_counts)
# 这里我们仅仅打印前10个测试数据的预测 及其真实类别
for prediction, truth in zip(predicted[:10], y_test[:10]):
print(prediction, truth)
运行结果
soc.religion.christian soc.religion.christian
alt.atheism talk.religion.misc
talk.politics.mideast talk.politics.mideast
rec.sport.baseball misc.forsale
talk.politics.mideast talk.politics.mideast
comp.sys.ibm.pc.hardware comp.os.ms-windows.misc
talk.politics.misc talk.politics.misc
sci.crypt sci.crypt
talk.politics.mideast talk.politics.mideast
sci.med sci.med
但是上面用肉眼看这10条数据还可以,但是测试集有5649条,我们不可能用肉眼去逐个对比的方法去检验训练的模型表现情况。好在sklearn为我们提供了检验方法,sklearn.metrics中含有分类报告函数classification_report()
from sklearn.metrics import classification_report
from sklearn.metrics import precision_recall_fscore_support
#按照分类labels分类输出准确率召回率F1值
print(classification_report(y_test, predicted, labels=dataset.label.unique()))
运行结果
precision recall f1-score support
alt.atheism 0.87 0.89 0.88 262
comp.graphics 0.63 0.89 0.74 297
comp.os.ms-windows.misc 1.00 0.13 0.23 302
comp.sys.ibm.pc.hardware 0.61 0.86 0.72 286
comp.sys.mac.hardware 0.90 0.86 0.88 266
comp.windows.x 0.79 0.85 0.82 286
misc.forsale 0.94 0.63 0.75 295
rec.autos 0.92 0.88 0.90 303
rec.motorcycles 0.98 0.92 0.95 282
rec.sport.baseball 0.98 0.95 0.96 293
rec.sport.hockey 0.97 0.98 0.97 296
sci.crypt 0.71 0.98 0.83 258
sci.electronics 0.90 0.78 0.84 305
sci.med 0.94 0.94 0.94 296
sci.space 0.91 0.91 0.91 314
soc.religion.christian 0.69 0.97 0.81 297
talk.politics.guns 0.83 0.97 0.89 278
talk.politics.mideast 0.81 0.95 0.88 283
talk.politics.misc 0.88 0.85 0.87 256
talk.religion.misc 0.98 0.41 0.57 194
avg / total 0.86 0.83 0.82 5649
3. 预处理 和 特征抽取
可能大家都知道,在文本数据分词后,其中混杂着很多无意义的停止词,这些词语在构建特征矩阵过程中我们要剔除掉。那么在构建特征矩阵其实还有一些需要注意的,比如有时候数字也是很重要的特征,这时候我们可能需要使用正则表达式对这些数字进行匹配并用如NUM这种词语去代替。下面我们看一个例子
test = ['p2p','me2','just4you','4 k+','500 pounds','300pound']
test中实际上有两种数字,
第一种是象征词语的如"p2p" 、"me2" 、"just4you"
第二种就是我们需要去抽取的数字特征
如"4 k+"、 "500 pounds"、 "300pound"
那么我们怎么提取第二种这种真正的数字特征呢,防止第一种假的”数字“混入数字特征中
import re
test = ['p2p','me2','just4you','4 k+','500 pounds','300pound']
print('我们先使用\d+去匹配看看结果')
print([re.findall(r'\d+', t) for t in test])
#发现该正则表达式能满足我们需要
print(r'再用\b\d+\b试试')
print([re.findall(r'\b\d+\b', t) for t in test])
print('将数字特征统一替换为NUM')
print([re.sub(r'\b\d+\b', 'NUM',t) for t in test])
运行结果
我们先使用\d+去匹配看看结果
[['2'], ['2'], ['4'], ['4'], ['500'], ['300']]
再用\b\d+\b试试
[[], [], [], ['4'], ['500'], []]
将数字特征统一替换为NUM
['p2p', 'me2', 'just4you', 'NUM k+', 'NUM pounds', '300pound']
因此,我们定义数字特征预处理函数
import re
def normalize_numbers(s):
"""
将数字特征统一替换为NUM
"""
return re.sub(r'\b\d+\b', 'NUM', s)
#CountVectorizer和TfidfVectorizer都有preprocessor参数,该参数传入预处理函数。
#也含有停用词处理参数stop_words
cv = CountVectorizer(preprocessor=normalize_numbers, stop_words='english')
我们再重复之前的CountVectorizer,重点知识多重复
#fit_transform是fit和transform两种方法的简写
#fit方法用于构建特征空间(也就是构建词典)
#transform方法使用该空间将文本数据转化为特征矩阵
X_train_counts = cv.fit_transform(X_train)
#用学到的特征空间去对测试集数据进行转换为特征矩阵
X_test_counts = cv.transform(X_test)
现在我们再使用贝叶斯分类器训练数据,看看模型是否得到改善。结果发现发现好像整体未得到改善,
estimator = MultinomialNB()
estimator.fit(X_train_counts, y_train)
predicted = estimator.predict(X_test_counts)
print(classification_report(y_test, predicted, labels=dataset.label.unique()))
运行结果
precision recall f1-score support
alt.atheism 0.87 0.89 0.88 262
comp.graphics 0.63 0.89 0.74 297
comp.os.ms-windows.misc 1.00 0.13 0.23 302
comp.sys.ibm.pc.hardware 0.61 0.86 0.72 286
comp.sys.mac.hardware 0.90 0.86 0.88 266
comp.windows.x 0.79 0.85 0.82 286
misc.forsale 0.94 0.63 0.75 295
rec.autos 0.92 0.88 0.90 303
rec.motorcycles 0.98 0.92 0.95 282
rec.sport.baseball 0.98 0.95 0.96 293
rec.sport.hockey 0.97 0.98 0.97 296
sci.crypt 0.71 0.98 0.83 258
sci.electronics 0.90 0.78 0.84 305
sci.med 0.94 0.94 0.94 296
sci.space 0.91 0.91 0.91 314
soc.religion.christian 0.69 0.97 0.81 297
talk.politics.guns 0.83 0.97 0.89 278
talk.politics.mideast 0.81 0.95 0.88 283
talk.politics.misc 0.88 0.85 0.87 256
talk.religion.misc 0.98 0.41 0.57 194
avg / total 0.86 0.83 0.82 5649
既然CountVectorizer即使使用了stop_words和预处理数字特征仍然改善不大(没有任何改善),那么我们再试试TfidfVectorizer这种更能体现词语代表性的特征抽取方法。
from sklearn.feature_extraction.text import TfidfVectorizer
# preprocessor预处理数据
tv = TfidfVectorizer(preprocessor=normalize_numbers, stop_words='english')
X_train_tf = tv.fit_transform(X_train)
X_test_tf = tv.transform(X_test)
estimator2 = MultinomialNB()
estimator2.fit(X_train_tf, y_train)
predicted = estimator2.predict(X_test_tf)
print(classification_report(y_test, predicted, labels=dataset.label.unique()))
运行结果
precision recall f1-score support
alt.atheism 0.88 0.83 0.85 262
comp.graphics 0.88 0.83 0.85 297
comp.os.ms-windows.misc 0.84 0.85 0.85 302
comp.sys.ibm.pc.hardware 0.74 0.83 0.78 286
comp.sys.mac.hardware 0.87 0.92 0.90 266
comp.windows.x 0.93 0.87 0.90 286
misc.forsale 0.90 0.74 0.81 295
rec.autos 0.94 0.92 0.93 303
rec.motorcycles 0.94 0.95 0.95 282
rec.sport.baseball 0.95 0.97 0.96 293
rec.sport.hockey 0.95 0.99 0.97 296
sci.crypt 0.76 0.98 0.85 258
sci.electronics 0.94 0.81 0.87 305
sci.med 0.97 0.94 0.95 296
sci.space 0.93 0.96 0.95 314
soc.religion.christian 0.65 0.98 0.79 297
talk.politics.guns 0.79 0.98 0.88 278
talk.politics.mideast 0.94 0.97 0.95 283
talk.politics.misc 0.99 0.68 0.80 256
talk.religion.misc 1.00 0.27 0.42 194
avg / total 0.89 0.87 0.87 5649
wow,之前使用CountVectorizer是0.86,现在我们使用TfidfVectorizer将准确率提高到0.89。
4. 模型选择
根据数据特点和分析任务需求特点,某些分类器可能会比其他的分类器效果更好。最近最大熵(Maximum Entropy)很火,能应用到很多机器学习任务中。注意,最大熵其实就是LogisticRegression,该算法比朴素贝叶斯慢很多。
from sklearn.linear_model import LogisticRegression
cv = CountVectorizer()
X_train_counts = cv.fit_transform(X_train)
X_test_counts = cv.transform(X_test)
LR = LogisticRegression()
LR.fit(X_train_counts, y_train)
predicted = LR.predict(X_test_counts)
print(classification_report(y_test, predicted, labels=dataset.label.unique()))
运行结果
precision recall f1-score support
alt.atheism 0.90 0.91 0.90 262
comp.graphics 0.82 0.84 0.83 297
comp.os.ms-windows.misc 0.85 0.87 0.86 302
comp.sys.ibm.pc.hardware 0.78 0.79 0.78 286
comp.sys.mac.hardware 0.88 0.89 0.89 266
comp.windows.x 0.89 0.87 0.88 286
misc.forsale 0.83 0.87 0.85 295
rec.autos 0.92 0.91 0.92 303
rec.motorcycles 0.96 0.93 0.94 282
rec.sport.baseball 0.92 0.96 0.94 293
rec.sport.hockey 0.97 0.98 0.97 296
sci.crypt 0.96 0.96 0.96 258
sci.electronics 0.85 0.86 0.86 305
sci.med 0.91 0.94 0.92 296
sci.space 0.94 0.95 0.95 314
soc.religion.christian 0.90 0.95 0.93 297
talk.politics.guns 0.92 0.93 0.93 278
talk.politics.mideast 0.98 0.95 0.96 283
talk.politics.misc 0.95 0.84 0.89 256
talk.religion.misc 0.91 0.78 0.84 194
avg / total 0.90 0.90 0.90 5649
哇,LogisticRegression使用 CountVectorizer准确率就已经达到0.9。
最后,我们使用Kmeans算法做一个聚类分析。
注意聚类分析得出的结果是需要人去解读。
from sklearn.cluster import KMeans
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from sklearn import metrics
from sklearn import preprocessing
#random_state是随机状态码,这样保证下次运行与这次运行结果一致
X_train, X_test, y_train, y_test = train_test_split(dataset.text, dataset.label, test_size=0.3, random_state=100)
# 这里我主动将特征空间压缩到200维,也就是只要两百个词语作为特征词
cv = CountVectorizer(max_features=200)
X_train_counts = cv.fit_transform(X_train)
X_test_counts = cv.transform(X_test)
#因为知道数据集是20类,我们将聚类数n_clusters设为20
#random_state是随机状态码,这样保证下次运行与这次运行结果一致
kmeans = KMeans(n_clusters=20, random_state=100)
kmeans.fit(X_train_counts)
#查看所有训练集中所有数据对应的聚类标签
print(kmeans.labels_)
print(kmeans.predict(X_test_counts))
运行结果
[19 19 19 ... 19 7 19]
[ 7 7 19 ... 19 8 19]
5. 交叉检验cross-validation
实际上之前我们只是将数据集分为训练集和测试集,并且测试集占比30%。这就导致一个问题,我们没有充分利用测试集的数据,导致可供训练的数据变少。为了能够充分利用数据,又能对训练进行测试。这里有一种方法叫做交叉检验。
交叉检验是将dataset分成k等份的subsets后,每次取1份subset作为test_set,其余k-1份作为training_set。
from sklearn.model_selection import KFold
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import precision_recall_fscore_support
dataset = pd.read_csv('20news-18828.csv', header=None, delimiter=',', names=['label', 'text'])
X = dataset.text
y = dataset.label
#cross-validation 打乱顺序,5等分。
kf = KFold(n_splits=5, shuffle=True)
for train_index, test_index in kf.split(X):
X_train, X_test, y_train, y_test = X[train_index], X[test_index], y[train_index], y[test_index]
cv = CountVectorizer()
X_train_counts = cv.fit_transform(X_train)
X_test_counts = cv.transform(X_test)
clf = MultinomialNB()
clf.fit(X_train_counts, y_train)
predicted = clf.predict(X_test_counts)
p, r, f1, _ = precision_recall_fscore_support(y_test, predicted, average='macro')
print("\n准确率: {0}, 召回率: {1}, F1值: {2}".format(p, r, f1))
运行结果
准确率: 0.8713883483238088, 召回率: 0.8444331487200941, F1值: 0.8381792939819347
准确率: 0.8531060729419465, 召回率: 0.8312135932152993, F1值: 0.8191609558805071
准确率: 0.8635337763830291, 召回率: 0.8327561380347246, F1值: 0.8179305639933958
准确率: 0.863773840212702, 召回率: 0.8366519217276238, F1值: 0.8242302926326295
准确率: 0.8691118498586106, 召回率: 0.8422442830086692, F1值: 0.8300984911679719
6. 总结
本文好像不难吧,认真看下来相信大家能使用sklearn做一些有趣的数据分析。之前分享过的标注的文本数据 《上百G文本数据集等你来认领|免费领取》,大家可以用来玩玩。